//===============================================================================
// Microsoft patterns & practices
// SharePoint Guidance version 2.0
//===============================================================================
// Copyright (c) Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===============================================================================
// The example companies, organizations, products, domain names,
// e-mail addresses, logos, people, places, and events depicted
// herein are fictitious.  No association with any real company,
// organization, product, domain name, email address, logo, person,
// places, or events is intended or should be inferred.
//===============================================================================
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Permissions;
using System.Text;
using System.Xml.Serialization;
using Microsoft.Practices.SPG.Common.ServiceLocation;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Security;

namespace Microsoft.Practices.SPG.Common.Configuration
{
    /// <summary>
    /// An implementation of the IConfigManager, that can read and write config settings in a hierarchical and typesafe way. 
    /// </summary>
    [SharePointPermissionAttribute(SecurityAction.InheritanceDemand, ObjectModel = true)]
    [SharePointPermissionAttribute(SecurityAction.LinkDemand, ObjectModel = true)] 
    public class ConfigManager : IConfigManager
    {
        private readonly SPContext currentContext;

        private IPropertyBag defaultPropertyBag;
        private readonly IConfigSettingSerializer configSettingSerializer;

        /// <summary>
        /// Gets the default property bag to use if no property bag is specified. If not overwritten in the constructor, this value will use 
        /// SPcontext.Current.Web.
        /// </summary>
        /// <value>The default property bag.</value>
        [SharePointPermissionAttribute(SecurityAction.InheritanceDemand, ObjectModel = true)]
        [SharePointPermissionAttribute(SecurityAction.LinkDemand, ObjectModel = true)] 
        public IPropertyBag GetDefaultPropertyBag()
        {
            if (this.defaultPropertyBag == null)
            {
                if (currentContext == null)
                {
                    throw new NoSharePointContextException("The SPContext was not found. The default property bag needs access to the SPContext.Current because it wants to access the current web. ");
                }

                this.defaultPropertyBag = new SPWebPropertyBag(currentContext.Web);
            }

            return this.defaultPropertyBag;
        }

        /// <summary>
        /// Determines if a configvalue with the specified key can be found in config.
        /// Will recursively look up to parent propertybags to find the key.
        /// </summary>
        /// <param name="key">the specified key</param>
        /// <returns>
        /// 	<c>true</c> if the specified key contains key; otherwise, <c>false</c>.
        /// </returns>
        public bool ContainsKey(string key)
        {
            return ContainsKey(key, GetDefaultPropertyBag());
        }

        /// <summary>
        /// Determines if a configvalue with the specified key can be found in the specified propertybag.
        /// Will recursively look up to parent propertybags to find the key.
        /// </summary>
        /// <param name="key">The key.</param>
        /// <param name="propertyBag">The property bag.</param>
        /// <returns>
        /// 	<c>true</c> if the specified key contains key; otherwise, <c>false</c>.
        /// </returns>
        public bool ContainsKey(string key, IPropertyBag propertyBag)
        {
            if (propertyBag.Contains(key))
                return true;

            IPropertyBag parent = propertyBag.GetParent();
            if (parent == null)
                return false;

            return ContainsKey(key, parent);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ConfigManager"/> class.
        /// </summary>
        /// <param name="defaultPropertyBag">The default property bag to use if no propertybag is specified.</param>
        [SharePointPermissionAttribute(SecurityAction.InheritanceDemand, ObjectModel = true)]
        [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true)] 
        public ConfigManager(IPropertyBag defaultPropertyBag) : this(defaultPropertyBag, new ConfigSettingSerializer())
        {
            currentContext = SPContext.Current;
            this.defaultPropertyBag = defaultPropertyBag;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ConfigManager"/> class.
        /// </summary>
        /// <param name="defaultPropertyBag">The default property bag to use if no property bag is specified.</param>
        /// <param name="configSettingSerializer">The config setting serializer.</param>
        [SharePointPermissionAttribute(SecurityAction.InheritanceDemand, ObjectModel = true)]
        [SharePointPermissionAttribute(SecurityAction.LinkDemand, ObjectModel = true)] 
        public ConfigManager(IPropertyBag defaultPropertyBag, IConfigSettingSerializer configSettingSerializer)
        {
            if (configSettingSerializer == null) throw new ArgumentNullException("configSettingSerializer");
            currentContext = SPContext.Current;
            this.defaultPropertyBag = defaultPropertyBag;
            this.configSettingSerializer = configSettingSerializer;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ConfigManager"/> class.
        /// </summary>
        [SharePointPermissionAttribute(SecurityAction.InheritanceDemand, ObjectModel = true)]
        [SharePointPermissionAttribute(SecurityAction.LinkDemand, ObjectModel = true)] 
        public ConfigManager() : this(null, new ConfigSettingSerializer())
        {
            
        }

        /// <summary>
        /// Remove a configvalue from a specified propertybag and all it's children. 
        /// </summary>
        /// <param name="key">The configsetting to remove</param>
        /// <param name="propertyBag">The propertyBag to remove it from</param>
        [SharePointPermissionAttribute(SecurityAction.InheritanceDemand, ObjectModel = true)]
        [SharePointPermissionAttribute(SecurityAction.LinkDemand, ObjectModel = true)]
        public void Remove(string key, IPropertyBag propertyBag)
        {
            if (propertyBag == null) throw new ArgumentNullException("propertyBag");
            propertyBag.Remove(key);
            propertyBag.Update();
        }

        /// <summary>
        /// Read a config value based on the key, from the default propertybag: SPContext.Current.Web
        /// </summary>
        /// <typeparam name="TValue">The type of the value to read. </typeparam>
        /// <param name="key">The key associated with the config value</param>
        /// <returns>The value</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter"), SharePointPermissionAttribute(SecurityAction.InheritanceDemand, ObjectModel = true)]
        [SharePointPermissionAttribute(SecurityAction.LinkDemand, ObjectModel = true)]
        public TValue GetByKey<TValue>(string key)
        {
            return GetByKey<TValue>(key, this.GetDefaultPropertyBag());
        }

        /// <summary>
        /// Read a config value based on the key, from the specified propertybag.
        /// </summary>
        /// <typeparam name="TValue">The type of the value to read.</typeparam>
        /// <param name="key">The key associated with the config value</param>
        /// <param name="propertyBag">The property bag to read values from.</param>
        /// <returns>The value</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter"), SharePointPermissionAttribute(SecurityAction.InheritanceDemand, ObjectModel = true)]
        [SharePointPermissionAttribute(SecurityAction.LinkDemand, ObjectModel = true)]
        public TValue GetByKey<TValue>(string key, IPropertyBag propertyBag)
        {
            if (propertyBag == null) throw new ArgumentNullException("propertyBag");
            
            string configValueAsString = string.Empty;
            try
            {
                configValueAsString = GetValueFromPropertyBag(propertyBag, key);
                return (TValue)configSettingSerializer.Deserialize(typeof(TValue), configValueAsString);
            }
            catch(ConfigurationException)
            {
                throw;
            }
            catch (Exception ex)
            {
                string errorMessage = string.Format(CultureInfo.CurrentCulture
                                                    , "Configsetting with key '{0}' could not be retrieved. The configured value could not be converted from '{1}' to an instance of '{2}'. The technical exception was: {3}: {4}"
                                                    , key
                                                    , configValueAsString
                                                    , typeof(TValue)
                                                    , ex.GetType().FullName
                                                    , ex.Message);

                throw new ConfigurationException(errorMessage, ex);
            }

        }

        private string GetValueFromPropertyBag(IPropertyBag propertyBag, string key)
        {
            if (propertyBag.Contains(key))
            {
                return propertyBag[key];
            }

            IPropertyBag parent = propertyBag.GetParent();
            if (parent == null)
            {
                string exceptionMessage = string.Format(CultureInfo.CurrentCulture, "There was no value configured for key '{0}' in a propertyBag with level '{1}' or above.", key, propertyBag.Level);
                throw new ConfigurationException(exceptionMessage);
            }

            return GetValueFromPropertyBag(parent, key);
        }

        /// <summary>
        /// Sets a config value for a specific key in the specified propertybag
        /// </summary>
        /// <param name="key">The key for this config setting</param>
        /// <param name="value">The value to give this config setting</param>
        /// <param name="propertyBag">the propertybag to store the setting in</param>
        [SharePointPermissionAttribute(SecurityAction.InheritanceDemand, ObjectModel = true)]
        [SharePointPermissionAttribute(SecurityAction.LinkDemand, ObjectModel = true)]
        public void SetByKey(string key, object value, IPropertyBag propertyBag)
        {
            if (propertyBag == null) throw new ArgumentNullException("propertyBag");
            string valueAsString = "(null)"; ;
            string typeName = "(null)";

            try
            {
                if (value == null)
                {
                    propertyBag[key] = null;
                }
                else
                {
                    valueAsString = value.ToString();
                    typeName = value.GetType().FullName;
                    propertyBag[key] = this.configSettingSerializer.Serialize(value.GetType(), value);
                }
                propertyBag.Update();
            }
            catch (Exception ex)
            {
                string exceptionMessage = string.Format(CultureInfo.CurrentCulture
                                                        , "Configsetting with key '{0}' could not be set '{1}' with type '{2}'. The technical exception was: {3}: {4}"
                                                        , key
                                                        , valueAsString
                                                        , typeName
                                                        , ex.GetType().FullName
                                                        , ex.Message);

                throw new ConfigurationException(exceptionMessage, ex);
            }

        }

        /// <summary>
        /// Sets a config value for a specific key in the specified propertybag
        /// </summary>
        /// <param name="key">The key for this config setting</param>
        /// <param name="value">The value to give this config setting</param>
        /// <param name="propertyBag">the propertybag to store the setting in</param>
        [SharePointPermissionAttribute(SecurityAction.InheritanceDemand, ObjectModel = true)]
        [SharePointPermissionAttribute(SecurityAction.LinkDemand, ObjectModel = true)]
        public void SetByKey(string key, object value, SPWeb propertyBag)
        {
            if (propertyBag == null) throw new ArgumentNullException("propertyBag");
            this.SetByKey(key, value, new SPWebPropertyBag(propertyBag));
        }

        /// <summary>
        /// Sets a config value for a specific key in the specified propertybag
        /// </summary>
        /// <param name="key">The key for this config setting</param>
        /// <param name="value">The value to give this config setting</param>
        /// <param name="propertyBag">the propertybag to store the setting in</param>
        [SharePointPermissionAttribute(SecurityAction.InheritanceDemand, ObjectModel = true)]
        [SharePointPermissionAttribute(SecurityAction.LinkDemand, ObjectModel = true)]
        public void SetByKey(string key, object value, SPSite propertyBag)
        {
            if (propertyBag == null) throw new ArgumentNullException("propertyBag");
            this.SetByKey(key, value, new SPSitePropertyBag(propertyBag));
        }

        /// <summary>
        /// Sets a config value for a specific key in the specified propertybag
        /// </summary>
        /// <param name="key">The key for this config setting</param>
        /// <param name="value">The value to give this config setting</param>
        /// <param name="propertyBag">the propertybag to store the setting in</param>
        [SharePointPermissionAttribute(SecurityAction.InheritanceDemand, ObjectModel = true)]
        [SharePointPermissionAttribute(SecurityAction.LinkDemand, ObjectModel = true)]
        public void SetByKey(string key, object value, SPWebApplication propertyBag)
        {
            if (propertyBag == null) throw new ArgumentNullException("propertyBag");
            this.SetByKey(key, value, new SPWebAppPropertyBag(propertyBag));
        }

        /// <summary>
        /// Sets a config value for a specific key in the specified propertybag
        /// </summary>
        /// <param name="key">The key for this config setting</param>
        /// <param name="value">The value to give this config setting</param>
        /// <param name="propertyBag">the propertybag to store the setting in</param>
        [SharePointPermissionAttribute(SecurityAction.InheritanceDemand, ObjectModel = true)]
        [SharePointPermissionAttribute(SecurityAction.LinkDemand, ObjectModel = true)]
        public void SetByKey(string key, object value, SPFarm propertyBag)
        {
            if (propertyBag == null) throw new ArgumentNullException("propertyBag");
            this.SetByKey(key, value, new SPFarmPropertyBag(propertyBag));
        }
    }
}
